/*******************************************************************************
 *
 *    Copyright (c) Baina Info Tech Co. Ltd
 *
 *    MoboTap Client
 * 
 *    WebServiceClient
 *
 *    @author: chzhong
 *    @since:  2010-8-10
 *    @version: 1.0
 *
 ******************************************************************************/

package com.sdk.network;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONObject;

import android.text.TextUtils;

import com.sdk.formatter.StringUtil;
import com.sdk.util.Log;



/**
 * WebServiceClient of MoboTap Client.
 * @author chzhong
 *
 */
public abstract class WebServiceClient {

    /**
     * HttpClientConfig of MoboTap Client.
     * @author chzhong
     *
     */
    public static class HttpClientConfig {
        /**
         * The User Agent to use.
         */
        public String userAgent;
        /**
         * Whether use dummy SSL.
         */
        public boolean useDummySSL;


        /**
         * Determine whether this config only apply once.
         */
        public boolean once = true;
    }

    /**
     * Use secure connection.
     */
    protected static final String SECURE_HTTP_SCHEMA = "https";
    /**
     * GZip compression format.
     */
    protected static final String COMPRESS_FORMAT_GZIP = "gzip";
    /**
     * Deflate compression format.
     */
    protected static final String COMPRESS_FORMAT_DEFLATE = "deflate";
    /**
     * Disable compression.
     */
    protected static final String COMPRESS_FORMAT_NONE = null;

    /**
     * BUFFER_SIZE
     */
    public static final int BUFFER_SIZE = 8192;

    private static final String TAG = WebServiceClient.class.getSimpleName();

    /**
     * Decode {@linkplain InputStream} from entity.
     * @param entity the entity to decode.
     * @return the {@linkplain InputStream} decoded from the entity.
     */
    public static InputStream decodeEntityAsStream(final HttpEntity entity) {
        InputStream result = null;
        if (entity != null) {
            try {
                final Header contentEncoding = entity.getContentEncoding();
                Log.d(TAG, "Response encoding = %s.", contentEncoding);
                boolean compressed = false;
                InputStream compressStream = null;
                String encoding = null;
                if (contentEncoding != null) {
                    encoding = contentEncoding.getValue();
                    if (COMPRESS_FORMAT_GZIP.equalsIgnoreCase(encoding)) {
                        Log.d(TAG, "Wrapping result with gzip encoding.");
                        compressed = true;
                        compressStream = new GZIPInputStream(entity.getContent(), BUFFER_SIZE);
                    } else if(COMPRESS_FORMAT_DEFLATE.equalsIgnoreCase(encoding)) {
                        Log.d(TAG, "Wrapping result with deflate encoding.");
                        compressed = true;
                        compressStream = new InflaterInputStream(entity.getContent());
                    }
                }
                if (compressed && compressStream != null) {
                    if (TextUtils.isEmpty(encoding)) {
                        encoding = EntityUtils.getContentCharSet(entity);
                    }
                    Log.d(TAG, "Decompressing result...");
                    result = compressStream;
                } else {
                    result = entity.getContent();
                }
            } catch (final Exception e) {
                Log.e(TAG, "request error " + e.toString());
                e.printStackTrace();
            }
        }
        return result;
    }
    /**
     * Decode string from entity.
     * @param entity the entity to decode.
     * @return the string decoded from the entity.
     */
    public static String decodeEntityAsString(final HttpEntity entity) {
        String result = null;
        if (entity != null) {
            try {
                final Header contentEncoding = entity.getContentEncoding();
                Log.d(TAG, "Response encoding = %s.", contentEncoding);
                boolean compressed = false;
                InputStream compressStream = null;
                String encoding = null;
                if (contentEncoding != null) {
                    encoding = contentEncoding.getValue();
                    if (COMPRESS_FORMAT_GZIP.equalsIgnoreCase(encoding)) {
                        Log.d(TAG, "Wrapping result with gzip encoding.");
                        compressed = true;
                        compressStream = new GZIPInputStream(entity.getContent(), BUFFER_SIZE);
                    } else if(COMPRESS_FORMAT_DEFLATE.equalsIgnoreCase(encoding)) {
                        Log.d(TAG, "Wrapping result with deflate encoding.");
                        compressed = true;
                        compressStream = new InflaterInputStream(entity.getContent());
                    }
                }
                if (compressed && compressStream != null) {
                    if (TextUtils.isEmpty(encoding)) {
                        encoding = EntityUtils.getContentCharSet(entity);
                    }
                    Log.d(TAG, "Decompressing result...");
                    result = StringUtil.stringFromInputStream2(compressStream, encoding);
                } else {
                    result = EntityUtils.toString(entity);
                }
            } catch (final Exception e) {
                Log.e(TAG, "request error " + e.toString());
                e.printStackTrace();
            }
        }
        return result;
    }

    /**
     * Decode string from entity.
     * @param entity the entity to decode.
     * @return the boolean decoded from the entity.
     */
    public static boolean decodeEntityAsBoolean(final HttpEntity entity) {
        boolean result = false;
        final String value = decodeEntityAsString(entity);
        result = Boolean.valueOf(value);
        return result;
    }


    private String mCompressionFormat;

    private String mServerUrl;

    private final Map<String, HttpResponse> mRememberedResponses = new HashMap<String, HttpResponse>();
    private final Map<String, HttpClientConfig> mConfigs = new HashMap<String, HttpClientConfig>();


    /**
     * Initiate a new instance of {@link WebServiceClient}.
     * @param serverUrl the url of the server.
     */
    public WebServiceClient(final String serverUrl) {
        this.mServerUrl = serverUrl;
    }

    /**
     * Retrieve the compression format currently used.
     * @return the compression format currently used, or null if compression not enabled.
     */
    public final String getCompressionFormat() {
        return mCompressionFormat;
    }

    /**
     * Make sure the responses from the given url is remembered.
     * @param url the URL which reponse to remember.
     */
    protected final void rememberResponse(final String url) {
        synchronized (mRememberedResponses) {
            mRememberedResponses.put(url, null);
        }
    }


    private void saveResponse(final String url, final HttpResponse response) {
        synchronized (mRememberedResponses) {
            if (mRememberedResponses.containsKey(url)) {
                mRememberedResponses.put(url, response);
            }
        }
    }


    /**
     * Retrieve last remembered response from the URL, and never remember it again.
     * @param url the URL which response to remember.
     */
    protected final HttpResponse forgetResponse(final String url) {
        synchronized (mRememberedResponses) {
            return mRememberedResponses.remove(url);
        }
    }

    /**
     * Retrieve last remembered response from the URL.
     * @param url the URL which reponse to remember.
     */
    protected final HttpResponse peekResponse(final String url) {
        synchronized (mRememberedResponses) {
            return mRememberedResponses.get(url);
        }
    }

    /**
     * Retrieve the URL of the server.
     * @return the URL of the server.
     */
    public final String getServerUrl() {
        return mServerUrl;
    }

    /**
     * Get the content of the URL.
     * @param url the URL.
     * @return the content of the URL as {@linkplain HttpEntity}.
     */
    protected final HttpEntity getUrl(final String url) {
        HttpEntity result = null;
        HttpResponse response = null;
        try {
            final HttpClient httpClient = createHttpClient(url);

            final HttpGet get = new HttpGet(url);
            prepareRequest(get, url);
            Log.d(TAG, "GET content from url %s.", url);
            response = httpClient.execute(get);
            final int statusCode = response.getStatusLine().getStatusCode();
            Log.d(TAG, "Responsed from url %s : %d.", url, statusCode);
            if(HttpStatus.SC_OK == statusCode){
                final HttpEntity entity = response.getEntity();
                result = entity;
            } else {
                Log.e(TAG, "Server response error : %s ", response.getStatusLine());
            }
        } catch (final Exception e) {
            Log.e(TAG, "request error " + e.toString());
            e.printStackTrace();
        } finally {
            saveResponse(url, response);
        }
        return result;
    }



    /**
     * Retrieve the content of the URL as string.
     * @param url The URL to retrieve.
     * @return The content of the given URL, or null if error occurred.
     */
    protected final InputStream getUrlAsStream(final String url) {
        final HttpEntity entity = getUrl(url);
        final InputStream result = decodeEntityAsStream(entity);
        return result;
    }

    /**
     * Retrieve the content of the URL as string.
     * @param url The URL to retrieve.
     * @return The content of the given URL, or null if error occurred.
     */
    protected final String getUrlAsString(final String url) {
        final HttpEntity entity = getUrl(url);
        final String result = decodeEntityAsString(entity);
        return result;
    }

    /**
     * Create a new {@linkplain WebServiceUrlBuilder} used to build Web Service URLs.
     * @return a new {@linkplain WebServiceUrlBuilder} used to build Web Service URLs.
     */
    protected WebServiceUrlBuilder newUrlBuilder() {
        return newUrlBuilder(false);
    }


    /**
     * Create a new {@linkplain WebServiceUrlBuilder} used to build Web Service URLs.
     * @return a new {@linkplain WebServiceUrlBuilder} used to build Web Service URLs.
     */
    protected WebServiceUrlBuilder newUrlBuilder(final boolean secure) {
        String serverUrl = mServerUrl;
        if (secure && serverUrl.startsWith(HttpHost.DEFAULT_SCHEME_NAME)) {
            serverUrl = serverUrl.replaceFirst(HttpHost.DEFAULT_SCHEME_NAME, SECURE_HTTP_SCHEMA);
        }
        return new WebServiceUrlBuilder(serverUrl);
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     */
    protected final boolean postContent(final String url, final HttpEntity content) {
        HttpResponse response = null;
        try {
            final HttpClient httpClient = createHttpClient(url);

            final HttpPost post = new HttpPost(url);
            if (content != null) {
                post.setEntity(content);
            }
            prepareRequest(post, url);
            Log.d(TAG, "POST content to url %s.", url);
            response = httpClient.execute(post);
            final int statusCode = response.getStatusLine().getStatusCode();
            Log.d(TAG, "Responsed from url %s : %d.", url, statusCode);
            if(HttpStatus.SC_OK == statusCode){
                return true;
            } else {
                Log.e(TAG, "Server response error : %s ", response.getStatusLine());
            }
        } catch (final Exception e) {
            Log.e(TAG, "Failed to post content.");
            e.printStackTrace();
        } finally {
            saveResponse(url, response);
        }
        return false;
    }


    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     */
    protected final String postContentWithResult(final String url, final HttpEntity content) {
        String result = null;
        try {
            final HttpClient httpClient = createHttpClient(url);
            final HttpPost post = new HttpPost(url);
            if (content != null) {
                post.setEntity(content);
            }
            prepareRequest(post, url);
            Log.d(TAG, "POST content to url %s.", url);
            final HttpResponse response = httpClient.execute(post);
            final int statusCode = response.getStatusLine().getStatusCode();
            Log.d(TAG, "Responsed from url %s : %d.", url, statusCode);
            if(HttpStatus.SC_OK == statusCode){
                final HttpEntity entity = response.getEntity();
                result = decodeEntityAsString(entity);
            } else {
                Log.e(TAG, "Server response error : %s ", response.getStatusLine());
            }
        } catch (final Exception e) {
            Log.e(TAG, "Failed to post content.");
            e.printStackTrace();
        }
        return result;
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     */
    protected final boolean postContent(final String url) {
        return postContent(url, (HttpEntity)null);
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @param length the length of the content.
     */
    protected final boolean postContent(final String url, final InputStream content, final long length) {
        return postContent(url, new InputStreamEntity(content, length));
    }


    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @param encoding the encoding of the content.
     */
    protected final boolean postContent(final String url, final List<? extends NameValuePair> parameters) {
        try {
            return postContent(url, parameters, HTTP.UTF_8);
        } catch (final UnsupportedEncodingException e) {
            // We will never get here because UTF-8 is supported.
        }
        return false;
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @param encoding the encoding of the content.
     * @throws UnsupportedEncodingException
     */
    protected final boolean postContent(final String url, final List<? extends NameValuePair> parameters, final String encoding) throws UnsupportedEncodingException {
        return postContent(url, new UrlEncodedFormEntity(parameters, encoding));
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @param encoding the encoding of the content.
     */
    protected final boolean postContent(final String url, final String content) {
        try {
            return postContent(url, content, HTTP.UTF_8);
        } catch (final UnsupportedEncodingException e) {
            // We will never get here because UTF-8 is supported.
        }
        return false;
    }

    protected String postContentWithResult(final String url,final String content){
        String result = null;
        try {
            result = postContentWithResult(url, content, HTTP.UTF_8);
        } catch (final UnsupportedEncodingException e) {
            // TODO: handle exception
        }
        return result;
    }

    protected String postContentWithResult(final String url,final JSONObject contentObject){
        String postResult = null;
        if(null != contentObject){
            postResult = postContentWithResult(url,contentObject.toString());
        }
        return postResult;
    }

    protected String postContentWithResult(final String url,final JSONArray contentArray){
        String postResult = null;
        if(null != contentArray){
            postResult = postContentWithResult(url,contentArray.toString());
        }
        return postResult;
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @param encoding the encoding of the content.
     * @throws UnsupportedEncodingException
     */
    protected final boolean postContent(final String url, final String content, final String encoding) throws UnsupportedEncodingException {
        final StringEntity entity = new StringEntity(content, encoding);
        entity.setContentEncoding(encoding);
        return postContent(url, entity);
    }

    protected final String postContentWithResult(final String url,final String content,final String encoding) throws UnsupportedEncodingException{
        final StringEntity entity = new StringEntity(content, encoding);
        entity.setContentEncoding(encoding);
        return postContentWithResult(url,entity) ;
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @return the returned content.
     */
    protected final HttpEntity postContentForEntity(final String url, final HttpEntity content) {
        HttpResponse response = null;
        try {
            final HttpClient httpClient = createHttpClient(url);

            final HttpPost post = new HttpPost(url);
            if (content != null) {
                post.setEntity(content);
                if (content instanceof StringEntity) {
                    final StringEntity stringEntity = (StringEntity)content;
                    post.setHeader(stringEntity.getContentEncoding());
                    post.setHeader(stringEntity.getContentType());
                }
            }
            Log.d(TAG, "POST content to url %s.", url);
            prepareRequest(post, url);
            response = httpClient.execute(post);
            final int statusCode = response.getStatusLine().getStatusCode();
            Log.d(TAG, "Responsed from url %s : %d.", url, statusCode);
            if(HttpStatus.SC_OK == statusCode){
                return response.getEntity();
            } else {
                Log.e(TAG, "Server response error : %s ", response.getStatusLine());
            }
        } catch (final Exception e) {
            Log.e(TAG, "Failed to post content.");
            e.printStackTrace();
        } finally {
            saveResponse(url, response);
        }
        return null;
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @return the returned content.
     */
    protected final HttpEntity postContentForEntity(final String url) {
        return postContentForEntity(url, (HttpEntity)null);
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @param length the length of the content.
     */
    protected final HttpEntity postContentForEntity(final String url, final InputStream content, final long length) {
        return postContentForEntity(url, new InputStreamEntity(content, length));
    }


    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @param encoding the encoding of the content.
     */
    protected final HttpEntity postContentForEntity(final String url, final List<? extends NameValuePair> parameters) {
        try {
            return postContentForEntity(url, parameters, HTTP.UTF_8);
        } catch (final UnsupportedEncodingException e) {
            // We will never get here because UTF-8 is supported.
        }
        return null;
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @param encoding the encoding of the content.
     * @throws UnsupportedEncodingException
     */
    protected final HttpEntity postContentForEntity(final String url, final List<? extends NameValuePair> parameters, final String encoding) throws UnsupportedEncodingException {
        return postContentForEntity(url, new UrlEncodedFormEntity(parameters, encoding));
    }
    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @param encoding the encoding of the content.
     */
    protected final HttpEntity postContentForEntity(final String url, final String content) {
        try {
            return postContentForEntity(url, content, HTTP.UTF_8);
        } catch (final UnsupportedEncodingException e) {
            // We will never get here because UTF-8 is supported.
        }
        return null;
    }

    /**
     * Post content to the given url.
     * @param url The url to post to.
     * @param content the content to post.
     * @param encoding the encoding of the content.
     * @throws UnsupportedEncodingException
     */
    protected final HttpEntity postContentForEntity(final String url, final String content, final String encoding) throws UnsupportedEncodingException {
        final StringEntity entity = new StringEntity(content, encoding);
        entity.setContentEncoding(encoding);
        return postContentForEntity(url, entity);
    }

    /**
     * Prepare the request, add authorization headers.
     * @param request the request.
     * @param url the url.
     */
    protected void prepareRequest(final HttpRequest request, final String url, final Object...args) {
    }


    /**
     * Set the compression format.
     * @param format the compression format to use, or null to disable compression.
     */
    protected final void setCompressionFormat(final String format) {
        mCompressionFormat = format;
    }


    /**
     * Set the value of serverUrl.
     * @param serverUrl the serverUrl to set
     */
    protected final void setServerUrl(final String serverUrl) {
        this.mServerUrl = serverUrl;
    }

    /**
     * Determine whether dummy SSL should be enabled.
     * @return true if dummy SSL should be enabled, false otherwise.
     */
    protected boolean isDummySSL() {
        return false;
    }

    /**
     * Returns the default User Agent we should use.
     * @return the default User Agent we should use.
     */
    protected String getDefaultUserAgent() {
        return null;
    }

    /**
     * Creates an {@link HttpClient}.
     * @param url the URL to link to.
     * @return the {@link HttpClient} to use.
     */
    protected HttpClient createHttpClient(final String url) {
        String userAgent = getDefaultUserAgent();
        boolean useDummySSL = isDummySSL();
        final HttpClientConfig config = getHttpClientConfig(url);
        if (config != null) {
            useDummySSL = config.useDummySSL;
            userAgent = TextUtils.isEmpty(config.userAgent) ? userAgent : config.userAgent;
        }
        return HttpClient.newInstance(userAgent, useDummySSL);
    }

    /**
     * Retrieve the config.
     * @param url the URL.
     * @return the config.
     */
    private HttpClientConfig getHttpClientConfig(final String url) {
        final HttpClientConfig config = mConfigs.get(url);
        if (config != null && config.once) {
            mConfigs.remove(url);
        }
        return config;
    }


    /**
     * Change config to specified URL.
     * @param url the URL to link to.
     * @param userAgent the userAgent to use.
     * @param useDummySSL true to use dummy SSL.
     */
    protected final void setHttpClientConfig(final String url, final HttpClientConfig config) {
        mConfigs.put(url, config);
    }

    /**
     * Change config to specified URL for once.
     * @param url the URL to link to.
     * @param userAgent the userAgent to use.
     * @param useDummySSL true to use dummy SSL.
     */
    protected final void setHttpClientConfig(final String url, final String userAgent, final boolean useDummySSL) {
        setHttpClientConfig(url, userAgent, useDummySSL, true);
    }

    /**
     * Change config to specified URL for once.
     * @param url the URL to link to.
     * @param userAgent the userAgent to use.
     * @param useDummySSL true to use dummy SSL.
     */
    protected final void setHttpClientConfig(final String url, final String userAgent) {
        setHttpClientConfig(url, userAgent, isDummySSL(), true);
    }

    /**
     * Change config to specified URL for once.
     * @param url the URL to link to.
     * @param userAgent the userAgent to use.
     * @param useDummySSL true to use dummy SSL.
     */
    protected final void setHttpClientConfig(final String url, final boolean useDummySSL) {
        setHttpClientConfig(url, getDefaultUserAgent(), useDummySSL, true);
    }

    /**
     * Change config to specified URL.
     * @param url the URL to link to.
     * @param userAgent the userAgent to use.
     * @param useDummySSL true to use dummy SSL.
     */
    protected final void setHttpClientConfig(final String url, final String userAgent, final boolean useDummySSL, final boolean once) {
        final HttpClientConfig config  = new HttpClientConfig();
        config.userAgent = userAgent;
        config.useDummySSL = useDummySSL;
        config.once = once;
        setHttpClientConfig(url, config);
    }

    /**
     * Change config to specified URL forever.
     * @param url the URL to link to.
     * @param userAgent the userAgent to use.
     * @param useDummySSL true to use dummy SSL.
     */
    protected final void keepHttpClientConfig(final String url, final String userAgent) {
        setHttpClientConfig(url, userAgent, isDummySSL(), false);
    }

    /**
     * Change config to specified URL forever.
     * @param url the URL to link to.
     * @param userAgent the userAgent to use.
     * @param useDummySSL true to use dummy SSL.
     */
    protected final void keepHttpClientConfig(final String url, final boolean useDummySSL) {
        setHttpClientConfig(url, getDefaultUserAgent(), useDummySSL, false);
    }

}
